package com.example.messagequeue.mqserver.datacenter;

import com.example.messagequeue.commom.BinaryRool;
import com.example.messagequeue.commom.MqException;
import com.example.messagequeue.mqserver.core.MSGQueue;
import com.example.messagequeue.mqserver.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;

public class MessageFileManager {

    //定义一个内部类来表示该队列的统计信息
    //有限考虑使用static，静态内部类
    static public class Stat{
        public int totalCount;//总消息数
        public int validCount;//有效消息数
    }

    public void init(){

    }


    //预定消息文件所在的目录和文件名
    //用来获取到指定的队列对应的消息文件所在路径
    private String getQueueDir(String queueNmae){
        return "./data/" + queueNmae;
    }

    //用来获取该队列的消息数据文件路径
    //二进制文件使用txt作为后缀不太合适，可以使用bin或者dat
    private String getQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    //用来获取该队列的消息统计文件路径
    private String getQueueStatPath(String queueName){
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    private Stat readStat(String queueName){
        //由于当前的消息统计文件是文本文件，可以直接使用scanner来读取文件内容
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void wirteStat(String queueName, Stat stat){
        //使用PrintWriter来写文件
        //outputstream打开文件，默认情况下，会把原文件的内容直接清空，相当于覆盖重写
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //创建队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        //创建队列对应的消息目录
        File baseDir = new File(getQueueDir(queueName));
        if(!baseDir.exists()){
            boolean ok = baseDir.mkdirs();
            if(!ok){
                throw new IOException("创建目录失败。 baseDir=" + baseDir.getAbsolutePath());
            }
        }
        //创建队列消息数据
        File queueDataFiles = new File(getQueueDataPath(queueName));
        if (!queueDataFiles.exists()){
            boolean ok = queueDataFiles.createNewFile();
            if(!ok){
                throw new IOException("创建文件失败。 queueDataFiles=" + queueDataFiles.getAbsolutePath());
            }
        }
        //创建消息统计文件
        File queueStatFiles = new File(getQueueStatPath(queueName));
        if (!queueStatFiles.exists()){
            boolean ok = queueStatFiles.createNewFile();
            if (!ok){
                throw new IOException("创建文件失败。 queueStatFiles=" + queueStatFiles.getAbsolutePath());
            }
        }
        //给消息统计文件设置初始值
        Stat stat = new Stat();
        stat.validCount = 0;
        stat.totalCount = 0;
        wirteStat(queueName,stat);
    }

    //删除队列的目录和文件
    //先删除文件后删除目录
    //当队列被删除后，里面的目录和文件也要被删除
    public void destroyQueueFiles(String queueName) throws IOException {
        File queueDataFiles = new File(getQueueDataPath(queueName));
        boolean ok2 = queueDataFiles.delete();
        File queueStatFiles = new File(getQueueStatPath(queueName));
        boolean ok3 = queueStatFiles.delete();
        File baseDir = new File(getQueueDir(queueName));
        boolean ok1 = baseDir.delete();
        if(!ok1 || !ok2 || !ok3){
            throw new IOException("删除文件和目录失败。 baseDir=" + baseDir.getAbsolutePath());
        }
    }

    //检查队列的目录和文件是否存在
    public boolean checkFilesExits(String queueName){
//        File baseDir = new File(getQueueDir(queueName));
//        if(!baseDir.exists()){
//            return false;
//        }
        File queueDataFiles = new File(getQueueDataPath(queueName));
        if (!queueDataFiles.exists()){
            return false;
        }
        File queueStatFiles = new File(getQueueStatPath(queueName));
        if (!queueStatFiles.exists()){
            return false;
        }
        return true;

    }

    //把一个新的消息放入到队列对应的文件中去
    public void sendMessage(MSGQueue queue,Message message) throws MqException, IOException {
        // 1. 检查一下当前要写入的队列对应的文件是否存在.
        if (!checkFilesExits(queue.getName())) {
            throw new MqException("[MessageFileManager] 队列对应的文件不存在! queueName=" + queue.getName());
        }
        // 2. 把 Message 对象, 进行序列化, 转成二进制的字节数组.
        byte[] messageBinary = BinaryRool.toBytes(message);
        synchronized (queue) {
            // 3. 先获取到当前的队列数据文件的长度, 用这个来计算出该 Message 对象的 offsetBeg 和 offsetEnd
            // 把新的 Message 数据, 写入到队列数据文件的末尾. 此时 Message 对象的 offsetBeg , 就是当前文件长度 + 4
            // offsetEnd 就是当前文件长度 + 4 + message 自身长度.
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            // 通过这个方法 queueDataFile.length() 就能获取到文件的长度. 单位字节.
            message.setOffsetBeg(queueDataFile.length() + 4);
            message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);
            // 4. 写入消息到数据文件, 注意, 是追加写入到数据文件末尾.
            try (OutputStream outputStream = new FileOutputStream(queueDataFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    // 接下来要先写当前消息的长度, 占据 4 个字节的~~
                    dataOutputStream.writeInt(messageBinary.length);
                    // 写入消息本体
                    dataOutputStream.write(messageBinary);
                }
            }
            // 5. 更新消息统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            wirteStat(queue.getName(), stat);
        }
    }

    //这个是删除消息的方法，这里的删除是逻辑删除
    //将消息从队列中读出，然后将isvalid修改，在写回去，在修正统计文件
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue){
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()),"rw")){
                //读取对应的Message对象
                byte[] bodySrc = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bodySrc);
                //把当前的二进制文件转化为message对象
                Message disMessage =(Message) BinaryRool.fromBytes(bodySrc);
                //把isvalid设置为无效
                disMessage.setIsValid((byte) 0x0);
                //重新写入文件
                byte[] bodyDesc = BinaryRool.toBytes(disMessage);
                //不管是读还是写，都会造成光标的移动，需要调整好光标位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bodyDesc);

                //修改统计文件
                Stat stat = readStat(queue.getName());
                if(stat.validCount > 0){
                    stat.validCount -= 1;
                }
                wirteStat(queue.getName(),stat);
            }
        }

    }
    //使用这个方法，从文件中读取出所有的消息内容，加载到内存中（具体说是放到一个链表中）
    //这个方法在程序启动时调用
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        int currentOffset = 0;
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)){
                //一个文件中势必有很多消息，所以需要循环取出
                while (true){
                    //一个消息的长度
                    //这个读取可能会读到文件尾，当读到文件末尾时，会报EQFException异常
                    int messageSize = dataInputStream.readInt();
                    //按照这个长度读取消息内容
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    //判断预期长度和实际长度是否一致
                    if(messageSize != actualSize){
                        throw  new MqException("[MessageFileManager] 文件格式错误！queueName = " + queueName);
                    }
                    Message message = (Message) BinaryRool.fromBytes(buffer);
                    if(message.getIsValid() != 0x1){
                        currentOffset += (messageSize + 4);
                        continue;
                    }
                    //有效数据则添加到链表中，填入前要把offsetBeg和offsetEnd填好
                    //需要手动计算光标位置
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    currentOffset += (messageSize + 4);
                    messages.add(message);
                }
            }catch (EOFException e){
                System.out.println("[MessageFileManager] 恢复message数据完成");
            }
        }
        return messages;
    }

    //检测是否需要进行垃圾回收
    public boolean checkGC(String queueName){
        Stat stat = readStat(queueName);
        if(stat.totalCount > 2000 && (double)stat.validCount / (double) stat.totalCount < 0.5){
            return true;
        }
        return false;
    }

    //changjain
    private String getQueueDataNewPath(String queueName){
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

    //通过这个方法，真正执行消息数据文件的垃圾回收操作
    //使用复制算法完成
    //创建一个新的文件queue_data_new.txt
    //把之前消息数据文件中的有效消息读出来，写到新的文件中
    //删除旧的文件，再把新的文件名改为queue_data.txt
    //同时要记得更新消息统计文件
    public void gc(MSGQueue queue) throws MqException, IOException, ClassNotFoundException {
        synchronized (queue){
            //统计gc要执行的时间
            long gcBeg = System.currentTimeMillis();
            File queueDataNewPath = new File(getQueueDataNewPath(queue.getName()));
            if(queueDataNewPath.exists()){
                //如果已经存在，则说明上次gc失败，程序意外崩溃
                throw new MqException("[MessageFileManager] gc时发现该队列的queue_data_new.txt已经存在! queue = " + queue.getName());
            }

            boolean ok = queueDataNewPath.createNewFile();
            if(!ok){
                throw new MqException("[MessageFileManager] 创建文件失败! queueDataNewFile = " + queueDataNewPath.getAbsolutePath());
            }

            //2读取源文件中有效的消息
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());

            //3将有效消息写入到新文件中
            try (OutputStream outputStream = new FileOutputStream(getQueueDataNewPath(queue.getName()))){
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    for (Message message : messages){
                        byte[] buffer = BinaryRool.toBytes(message);
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            //4删除旧的文件，并把新的文件进行重命名
            File queueDataOldFiles = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFiles.delete();
            if(!ok){
                throw new MqException("[MessageFileManager] 删除旧的文件失败! queueDataOldFiles = " + queueDataOldFiles.getAbsolutePath());
            }
            ok = queueDataNewPath.renameTo(queueDataOldFiles);
            if(!ok){
                throw new MqException("[MessageFileManager] 文件重命名失败! queueDataNewFiles = " + queueDataNewPath.getAbsolutePath() + "queueDataOldFiles = " +
                        queueDataOldFiles.getAbsolutePath());
            }

            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = stat.totalCount;
            wirteStat(queue.getName(), stat);

            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc执行完毕！queueName = " + queue.getName() + "time = " + (gcEnd - gcBeg) + "ms");
        }
    }

}
